Fullstack-Study

Exception & Memory

예외(Exception)

Error

종류

  1. Checked Exception

    • 컴파일 시 처리 강제 -> 처리하지 않으면 컴파일 실패
    • 외부 환경과 관련된 예외
    • 발생하면 컴파일 단계에서 문제를 잡아야 실행 가능
    • Checked Exception은 스프링 같은 프레임워크는 대부분 Checked -> Unchcked 변환해서 던지기 때문에 불필요하게 try-catch가 강제되지 않도록 RuntimeException으로 감쌈
    • ex : IOException, SQLException
  2. Unchecked Exception (RuntimeExecption 계열)

    • 런타임 시 발생 -> 컴파일러가 체크하지 않음
    • 프로그래밍 오류로 인한 예외
    • 발생해도 컴파일은 통과, 런타임 시점에서 해당 예외가 발생하면 그 시점 이후 코드 실행 중단
    • ex : NullPointerException, ArithmeticException

예외 처리 방법

  1. try-catch

    • 예외가 발생할 가능성이 있는 코드 블록에서 즉시 처리
    • 예외 발생 시 catch 블록으로 넘어가고, 이후 프로그램이 정상적으로 계속 실행 가능
    try {
        int a = 10 / 0;
    } catch(ArithmeticException e) {
        System.out.println("0으로 나눌 수 없습니다.");
    }
    
  2. throws

    • 예외를 자신이 처리하지 않고 호출한 쪽으로 위임
    • 메서드 선언에 작성 -> 호출자가 반드시 처리하거나 또 throws로 위임
    • main에서 throws문 작성시 JVM에게 예외 위임
    public void readFile() throws IOException {
        FileReader fr = new FileReader("test.txt");
    }
    
  3. try-with-resources

    • 자원을 자동으로 닫아주고, 예외가 발생해도 안전하게 처리
    • AutoCloaseable 구현 객체에 적용 -> finally 없이 자동 close
    • 컴파일러가 close() 호출을 try-finally로 변환해서 처리
    try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
        String line = br.readLine();
    } catch (IOException e) {
        e.printStackTrace();
    }
    

스택(Stack)과 예외 처리


메모리(Memory)

JVM 메모리 구조

  1. Stack(스택)

    • 메서드 호출 시 생성되는 메모리 영역
    • 지역변수, 매개 변수, 임시 값, 객체 주소 저장
    • 각 스레드마다 독립적으로 존재
    • LIFO 구조 : 마지막 호출한 메서드가 가장 먼저 반환
    • 예외 발생 시 stack trace 기록 -> 어디서 예외 발생했는지 추적 가능
    • 자동 해제 : 메서드 종료 시 스택 프레임 제거
  2. Heap(힙)

    • 동적 메모리 영역
    • 객체, 배열, 컬렉션 등 저장
    • 여러 스레드가 공유 가능
    • new키워드로 객체 생성 시 할당
    • 예외 발생 시에도 힙 메모리는 유지 -> GC 필요
    • 참조가 남아있다면 객체는 삭제되지 않고 메모리 누수 가능
  3. Method Area(메소드 영역)

    • 클래스 정보, 상수, static 변수, 메서드 코드 등 저장 공간
    • JVM이 프로그램 실행 시 클래스 로딩 시점에 생성
    • JVM 종료시 자동 제거, 개발자가 직접 삭제 불가
    • 내부에 Runtime Constant Pool 존재
    • 문자열 리터럴과 상수를 저장하는 공간
    • 같은 내용의 문자열이나 상수는 한 번만 저장되고, 여러 곳에서 공유 가능 => 같은 문자열을 반복 생성해도 실제 메모리는 한 개만 쓰고 주소만 공유

Heap 내부 세분화

  1. Young Generation

    • Eden 영역 : 새로 생성된 객체가 처음 할당되는 영역
    • 대부분의 객체가 여기서 생성-> 긍방 사라지는 객체 많음
    • Survivor 영역(S0,S1) : Eden에서 살아남은 객체를 잠시 저장
    • GC이후 Eden에서 살아남은 객체를 한 쪽 Survivor로 이동
    • 두 영역은 번갈아가며 사용
    • 특징 : Minor GC 자주 발생, GC 비용 낮음
  2. Old Generation

    • Young Gen에서 오래 살아남은 객체가 이동
    • GC 발생 빈도 낮음 -> Major GC
    • Major GC 비용 큼, 멈춤 시간(Stop-The-World) 길어짐
  3. Permanent Generation / Metaspace

    • 클래스 정보, 메서드 코드, static 변수, 런타임 상수 풀 저장
    • Java 8 이전 : Permanent Generation
    • Java 8 이후 : Metaspace(네이티브 메모리 사용)

Runtime Constant Pool

가비지 컬렉션(GC)

예외와 메모리 연결

메모리 누수(Memory Leak) 케이스

  1. 컬렉션에 객체 계속 추가하고 제거 안함

    • List, Map 같은 컬렉션은 참조를 계속 유지
    • GC는 참조가 없는 객체만 회수 가능 -> 컬렉션에 남아있는 객체는 절대 해제되지 않음
    • 결과적으로 JVM의 힙 점유가 늘어나고 OOM 발생
    • 예시
      List<byte[]> list = new ArrayList<>();
      while(true) {
          list.add(new byte[1024*1024]); // 계속 객체 생성, 참조 유지
      }
      
  2. Static 변수에 객체 계속 참조

    • static 변수는 클래스가 로딩되어 있는 동안 메모리에 상주 -> 프로그램 종료 전까지 GC가 회수하지 못함
    • 결과적으로 캐시에 불필요한 데이터가 남아 메모리 누수 발생
    • 예시
      static Map<Integer, String> cache = new HashMap<>();
      cache.put(1, "data"); // 계속 참조 유지
      
  3. Listener / Callback해제 안함

    • Listener

      • 이벤트를 감지하고 처리하는 객체
      • 주료 GUI 프로그램, 버튼 클릭, 마우스/키보드 이벤트에서 사용
      • 발신자(이벤트 발생 객체)가 수신자(리스너 객체)를 참조하여 이벤트 발생 시 메서드 호출
    • Callback

      • 특정 이벤트나 작업이 끝난 후 호출되는 메서드를 담고 있는 객체
    • GUI, 이벤트 시스템에서 리스너 등록 시 발신자 객체가 수신자 객체를 참조 -> 수신자 객체가 더 이상 필요해도 발신자 객체가 참조를 끊지 않으면 GC 못함

    • 오래 실행되는 프로그램에서 누적 가능

    • 예시

      button.addActionListener(myListener); // button이 myListener 참조
      // button.dispose() 후에도 myListener 참조 유지
      
  4. Inner Class/ Anonymous Class

    • 내부 클래스는 외부 클래스 객체 참조를 자동으로 가지고 있기 때문에 내부 클래스 인스턴스가 계속 참조되면 외부 클래스 객체도 GC 회수 불가
    • 예시
      class Outer {
          class Inner { }
      }
      
      Outer o = new Outer();
      Outer.Inner i = o.new Inner(); // i가 참조되는 동안 o도 GC 불가